#!/bin/bash -ue
printf '\e[8;25;92t'
printf '\e[0;40;97m'
printf "\e]0;Patch Installer\a"
set -ueo pipefail

fail() {
  >&2 echo -e "\nError / Ошибка: $1"
  exit 1
}

is_dry_run() {
  [[ ${dry_run+x} ]]
}

do_backup() {
  local bakfile="$1.bak"
  test -f "${bakfile}" || cp -n "$1" "${bakfile}"
}

do_patch_file_pattern() {
  local file=$1
  local pattern=$2
  local patch=$3
  local patchOffset=$4
  local escaped_pat
  local matches
  local match_cnt
  local offset

  is_dry_run || do_backup "${file}"
  escaped_pat=$(echo "${pattern}" | sed -e 's/xx/./g' -e 's/\([0-9a-fA-F]\{2\}\)/\\x\1/g' -e 's/ //g')

  matches=$(perl -0777 -nE 'my $cnt=0; while (m{('"${escaped_pat}"')}sg) { print "$-[0]\n"; \$cnt++; last if $cnt == 2; }' "${file}")
  match_cnt=$(awk 'END {print NR}' < <(printf "%s" "${matches}"))
  if [[ "${match_cnt}" -ne 1 ]]
  then
    fail "File ${file} pattern ${pattern} found ${match_cnt} matches, should be exactly one / В файле ${file} шаблон ${pattern} найден ${match_cnt} раз(а), должно быть ровно одно совпадение"
  fi

  offset="${matches%%:*}"
  if is_dry_run ;then
    printf "Dry run / Тестовый режим: not patching %s at offset 0x%x / не применяем патч к %s по смещению 0x%x\n" "$(basename "${file}")" "$((patchOffset + offset))" "$(basename "${file}")" "$((patchOffset + offset))"
  else
    shopt -s extglob
    echo -ne "\\x${patch//+( )/\\x}" | dd of="${file}" seek="$((patchOffset + offset))" bs=1 conv=notrunc status=none
    shopt -u extglob
  fi
}

get_files_to_patch() {
  local path="$1"
  echo "${path}/Contents/MacOS/Resolve"
  echo "${path}/Contents/Libraries/Fusion/libfusionsystem.dylib"
}

clear

do_perms() {
  local path="$1"
  local contents="${path}/Contents"
  echo "Please enter current user's password to give us write permissions for ${contents} / Введите пароль пользователя для получения прав записи в ${contents}"
  sudo chmod -R +a "${USER} allow list,add_file,search,add_subdirectory,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,file_inherit,directory_inherit" "$contents" || \
    fail "Failed to give current user write permissions to '${contents}' / Не удалось предоставить права записи пользователю для '${contents}'"
  until check_perms "${path}" 0
  do
    echo "Waiting for permissions to settle.. / Ожидаем применения прав доступа.."
    sleep 1
  done
}

check_sanity() {
  local path="$1"
  test -d "${path}" || fail "Directory ${path} does not exist! / Директория ${path} не существует!"
  IFS=$'\n' read -rd '' -a files < <(get_files_to_patch "${path}") || true
  for file in "${files[@]}"; do
    test -f "${file}" || fail "File ${file} does not exist! / Файл ${file} не существует!"
  done
  appVer="$(defaults read "${app}/Contents/Info.plist" CFBundleShortVersionString)"
  test -n "${appVer}" || fail "Could not determine app version / Не удалось определить версию приложения"
  printf "Resolve version / Версия Resolve: %s\n" "${appVer}"
}

check_perms() {
  local path="$1"
  local abort_on_err="$2"
  IFS=$'\n' read -rd '' -a files < <(get_files_to_patch "${path}") || true
  for file in "${files[@]}"; do
    if [[ ! -w "${file}" ]]; then
      if [[ "${abort_on_err}" -eq 0 ]] ; then
        return 1
      else
        fail "We don't have write permission to ${file}, please re-run with sudo or change directory permissions / Нет прав записи для ${file}, запустите скрипт с sudo или измените права доступа к директории"
      fi
    fi
  done
  return 0
}

version_gte() {
  local thanVer="$1"
  local lowestVer
  lowestVer="$(printf "%s\n%s" "${thanVer}" "${appVer}" | sort -V | head -1)"
  test "${thanVer}" == "${lowestVer}" || return 1
}

ensure_license() {
  local licfile licpath
  licfile="/Library/Application Support/Blackmagic Design/DaVinci Resolve/.license/blackmagic.lic"
  licpath=$(dirname "${licfile}")
  mkdir -p "${licpath}"
  test -f "${licfile}" || cat <<EOF >"${licfile}"
LICENSE blackmagic davinciresolvestudio 999999 permanent uncounted
  hostid=ANY issuer=CGP customer=CGP issued=28-dec-2023
  akey=0000-0000-0000-0000 _ck=00 sig="00"
EOF
}

do_codesign() {
  local path="$1"
  codesign --force --sign - --deep "${path}" || fail "Error during codesign, make sure your application path (${path}) is correct and that you have write permissions / Ошибка при подписании кода, убедитесь, что путь к приложению (${path}) верен и у вас есть права записи"
}

do_usage() {
  cat <<EOF
Usage / Использование: $0 [options / опции] [path_to_DaVinci_Resolve.app / путь_к_приложению]
  Options / Опции:
    -n|--dry-run   : Check if patches apply, don't actually patch anything / Проверить применимость патчей без фактического изменения файлов
    -x|--verbose   : Debug mode / Режим отладки
    -h|--help      : Show this message / Показать это сообщение
  Examples / Примеры:
    $0 
      - Apply patches to application at the default path '/Applications/DaVinci Resolve/DaVinci Resolve.app' / Применить патчи к приложению по стандартному пути
    $0 --dry-run '/Applications/DaVinci Resolve 19/DaVinci Resolve.app'
      - Check if patches apply to version installed to '/Applications/DaVinci Resolve 19' / Проверить применимость патчей для версии в указанной директории
  Version / Версия: 1.0.7
EOF
  exit 0
}

do_patch() {
  local app="$1"
  IFS=$'\n' read -rd '' -a files < <(get_files_to_patch "${app}") || true
  local resolve="${files[0]}"
  local fusion="${files[1]}"

  # Resolve and libfusion
  # patch out call to __rlm_auth_stat
  local arm_pat1="a0 03 5f f8 a1 83 5e f8 e3 0f 40 b9"
  local intel_pat1="31 c0 89 c2 48 8b 7d f0 48 8b 75 e8 8b 4d cc"


  # Resolve only
  # 2BMDCloudLicenseError(int, int)
  # 1OnBMDCloudLicenseError(int, int)
  # ignore license check results
  local arm_pat2="ff 43 01 d1 f6 57 02 a9 f4 4f 03 a9 fd 7b 04 a9 fd 03 01 91 f3 03 00 aa xx xx xx 97 xx xx xx xx xx xx xx 34 xx xx xx 97"
  local intel_pat2="55 48 89 e5 41 57 41 56 41 54 53 48 83 ec 20 48 89 fb e8 xx xx xx ff 48 89 c7"
  # patch out lic14 check in _rlmx_call_checkout
  local arm_pat3="80 00 00 34 xx 05 00 b4 14 00 80 52 02 00 00 14 34 00 80 52"
  local intel_pat3="41 xx xx 84 c0 0f 84 a6 00 00 00 4d 85 xx 74 08 45 31 xx e9 99 00 00 00"
  

  local arm_patch1="00 00 80 d2"
  local arm_patch2="20 00 80 52"
  # mov w20, #0
  local arm_patch3="14 00 80 52"

  # xor rax, rax
  local intel_patch1="48 31 c0 90 90"
  # mov al, 1
  local intel_patch2="b0 01 90 90 90"
  # mov R15B, 0x0
  local intel_patch3="00"
  
  # arm
  do_patch_file_pattern "${resolve}" "${arm_pat1}" "${arm_patch1}" 16
  do_patch_file_pattern "${fusion}" "${arm_pat1}" "${arm_patch1}" 16
  do_patch_file_pattern "${resolve}" "${arm_pat2}" "${arm_patch2}" 28
  do_patch_file_pattern "${resolve}" "${arm_pat2}" "${arm_patch2}" 40
  if version_gte "20.1"; then
    do_patch_file_pattern "${resolve}" "${arm_pat3}" "${arm_patch3}" 16
  fi
  # intel
  do_patch_file_pattern "${resolve}" "${intel_pat1}" "${intel_patch1}" 15
  do_patch_file_pattern "${fusion}" "${intel_pat1}" "${intel_patch1}" 15
  do_patch_file_pattern "${resolve}" "${intel_pat2}" "${intel_patch2}" 26
  do_patch_file_pattern "${resolve}" "${intel_pat2}" "${intel_patch2}" 47
  if version_gte "20.1"; then
    do_patch_file_pattern "${resolve}" "${intel_pat3}" "${intel_patch3}" 2
  fi
}

while [[ $# -gt 0 ]]; do
  case $1 in
    -n|--dry-run)
      dry_run=1
      shift # past value
      ;;
    -h|--help)
      do_usage
      # unreachable
      exit 0
      ;;
    -x|--verbose)
      set -x
      shift # past value
      ;;
  *)
    break
  esac
done 

app="${1:-/Applications/DaVinci Resolve/DaVinci Resolve.app}"
check_sanity "${app}"
if ! check_perms "${app}" 0 ; then
  do_perms "${app}"
  check_perms "${app}" 1
fi
do_patch "${app}"
if ! is_dry_run ; then
  ensure_license
  do_codesign "${app}"
  echo "All files Licensed successfully / Все файлы успешно лицензированы"
else
  echo "Dry run finished OK (no files were patched) / Тестовый режим завершён успешно (файлы не изменялись)"
fi